﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.UI;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint;
using WebPart = System.Web.UI.WebControls.WebParts.WebPart;

namespace Mazin.SPGoogleMapPart
{

    [ToolboxItem(false)]
    public class VisualWebPart : WebPart
    {
        private const string HttpUrl = "http://maps.googleapis.com";
        private const string HttpsUrl = "https://maps.googleapis.com";

        private readonly List<Marker> _markers = new List<Marker>();
        private string _addressColumnName;

        // map settings
        private double _centreLatitude;
        private double _centreLongitude;
        private string _descrColumnName;
        private int _height = 300;
        private bool _https;

        private bool _clusterer;
        private string _latitudeColumnName;
        private string _listItemIdParamName;
        private string _listQuery;
        private string _listUrl;
        private string _longitudeColumnName;
        private string _mapType = "HYBRID";
        private string _iconUrl;

        private string _titleColumnName;
        private string _webUrl;
        private int _width = 400;
        private int _zoom = 1;

        private bool _panControl;
        private bool _zoomControl;
        private bool _maptypeControl;
        private bool _streetviewControl;

        private string _apiKey;


        [Personalizable(PersonalizationScope.Shared),WebBrowsable(false)]
        public bool Https
        {
            get { return _https; }
            set { _https = value; }
        }

        [Personalizable(PersonalizationScope.Shared), WebBrowsable(false)]
        public bool UseClusterer
        {
            get { return _clusterer; }
            set { _clusterer = value; }
        }

        [Personalizable(PersonalizationScope.Shared),WebBrowsable(false)]
        public bool PanControl
        {
            get { return _panControl; }
            set { _panControl = value; }
        }


        [Personalizable(PersonalizationScope.Shared),WebBrowsable(false)]
        public bool ZoomControl
        {
            get { return _zoomControl; }
            set { _zoomControl = value; }
        }

        [Personalizable(PersonalizationScope.Shared),WebBrowsable(false)]
        public bool MapTypeControl
        {
            get { return _maptypeControl; }
            set { _maptypeControl = value; }
        }

        [Personalizable(PersonalizationScope.Shared), WebBrowsable(false)]
        public bool StreetViewControl
        {
            get { return _streetviewControl; }
            set { _streetviewControl = value; }
        }

        [WebBrowsable(false)]
        private string ApiUrl
        {
            get { return _https ? HttpsUrl : HttpUrl; }
        }

        [WebBrowsable(false)]
        public string ApiKey
        {
            get
            {
                return _apiKey;
            }
            set
            {
                _apiKey = value;
            }
        }

        [Category("Map"), Personalizable(PersonalizationScope.Shared),
         WebDescription("Latitude coordinate of the map centre"), WebBrowsable(false),
         WebDisplayName("Map Centre Latitude")]
        public double CentreLatitude
        {
            get { return _centreLatitude; }
            set { _centreLatitude = value; }
        }

        [WebDisplayName("Map Centre Longitude"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDescription("Longitude coordinate of the map centre"), Category("Map")]
        public double CentreLongitude
        {
            get { return _centreLongitude; }
            set { _centreLongitude = value; }
        }

        [WebDescription("Map Height"), Personalizable(PersonalizationScope.Shared), Category("Map"), WebBrowsable(false)
        , WebDisplayName("Map Height")]
        public int MapHeight
        {
            get { return _height; }
            set { _height = value; }
        }

        [WebDescription("Map Type"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Map type"), Category("Map")]
        public string MapType
        {
            get { return _mapType; }
            set { _mapType = value; }
        }

        [WebBrowsable(false), WebDisplayName("Map width"), Category("Map"), WebDescription("Map Width"),
         Personalizable(PersonalizationScope.Shared)]
        public int MapWidth
        {
            get { return _width; }
            set { _width = value; }
        }

        [WebDescription("Zoom Level"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Zoom level"), Category("Map")]
        public int ZoomLevel
        {
            get { return _zoom; }
            set { _zoom = value; }
        }

        [WebDescription("Icon URL"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Icon URL"), Category("Map")]
        public string IconUrl
        {
            get { return _iconUrl; }
            set { _iconUrl = value; }
        }

        [WebDescription("Name of address column"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Address column"), Category("Connection")]
        public string AddressColumn
        {
            get { return _addressColumnName; }
            set { _addressColumnName = value; }
        }

        [WebDescription("Name of longitude column"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Longitude column"), Category("Connection")]
        public string LongitudeColumn
        {
            get { return _longitudeColumnName; }
            set { _longitudeColumnName = value; }
        }

        [WebDescription("Name of latitude column"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Latitude column"), Category("Connection")]
        public string LatitudeColumn
        {
            get { return _latitudeColumnName; }
            set { _latitudeColumnName = value; }
        }

        [WebDescription("Name of description column"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Description column"), Category("Connection")]
        public string DescriptionColumn
        {
            get { return _descrColumnName; }
            set { _descrColumnName = value; }
        }

        [WebDescription("Name of title column"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Title column"), Category("Connection")]
        public string TitleColumn
        {
            get { return _titleColumnName; }
            set { _titleColumnName = value; }
        }

        [WebDescription("Url of Source list"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("List URL"), Category("Connection")]
        public string ListUrl
        {
            get { return _listUrl; }
            set { _listUrl = value; }
        }

        [WebDescription("Url of Source list"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Web URL"), Category("Connection")]
        public string WebUrl
        {
            get { return _webUrl; }
            set { _webUrl = value; }
        }

        [WebDescription("CAML query applied on the source list"), Personalizable(PersonalizationScope.Shared),
         WebBrowsable(false), WebDisplayName("List Query"), Category("Connection")]
        public string ListQuery
        {
            get { return _listQuery; }
            set { _listQuery = value; }
        }

        [WebDescription("Name of URL Id parameter for item to be displayed"),
         Personalizable(PersonalizationScope.Shared), WebBrowsable(false), WebDisplayName("List item Id parameter name")
        , Category("Connection")]
        public string ListItemIdParamName
        {
            get { return _listItemIdParamName; }
            set { _listItemIdParamName = value; }
        }

        [WebDescription("Mode to get data"), Personalizable(PersonalizationScope.Shared), WebBrowsable(false),
         WebDisplayName("Data mode"), Category("Connection")]
        public int DataMode { get; set; }

        /// <summary>
        ///   Method uses configured data source to get list of markers to be displayed
        /// </summary>
        private void GetData()
        {
            DataTable table = null;

            if (string.IsNullOrEmpty(_listUrl)) return;
            SPWeb web = null;
            try
            {
                web = SPContext.Current.Web;

                SPList list = web.GetList(_listUrl);
                var query = new SPQuery { Query = _listQuery };
                if (!string.IsNullOrEmpty(_listItemIdParamName) && Page.Request.Params[_listItemIdParamName] != null)
                {
                    string itemId = Page.Request.Params[_listItemIdParamName];
                    query.Query = string.Format("<Where><Eq><FieldRef Name=\"ID\" /><Value Type=\"Counter\">{0}</Value></Eq></Where>", itemId);
                }
                SPListItemCollection items = list.GetItems(query);
                table = items.GetDataTable();
            }
            finally
            {
                if (web != null && web != SPContext.Current.Web) web.Dispose();
            }


            if (table != null && (!string.IsNullOrEmpty(_addressColumnName) || (!string.IsNullOrEmpty(_latitudeColumnName) && !string.IsNullOrEmpty(_longitudeColumnName))))
            {
                bool hasLatitude = !string.IsNullOrEmpty(_latitudeColumnName) && table.Columns.Contains(_latitudeColumnName);
                bool hasLongitude = !string.IsNullOrEmpty(_longitudeColumnName) && table.Columns.Contains(_longitudeColumnName);

                bool hasAddress = !string.IsNullOrEmpty(_addressColumnName) && table.Columns.Contains(_addressColumnName);
                bool hasDescription = !string.IsNullOrEmpty(_descrColumnName) && table.Columns.Contains(_descrColumnName);
                bool hasTitle = !string.IsNullOrEmpty(_titleColumnName) && table.Columns.Contains(_titleColumnName);

                var tmps = new List<Marker>();
                Dictionary<string,Marker> addressCache = new Dictionary<string,Marker>();
  
                for (int i = 0; i < table.Rows.Count; i++)
                {
                    DataRow row = table.Rows[i];
                    var tmp = new Marker();

                    if (hasLatitude && hasLongitude) FillCoordinates(row, tmp);

                    if (hasAddress && tmp.Latitude == 0 && tmp.Longitude == 0)
                    {
                        tmp.Address = (string)row[_addressColumnName];
                        if (addressCache.ContainsKey(tmp.Address))
                        {
                            tmp.Latitude = addressCache[tmp.Address].Latitude;
                            tmp.Longitude = addressCache[tmp.Address].Longitude;
                        }
                    }

                    if (!string.IsNullOrEmpty(_iconUrl))
                    {
                        if (row.Table.Columns.Contains(_iconUrl))
                        {
                            tmp.Icon = row[_iconUrl] as string;
                            if (!string.IsNullOrEmpty(tmp.Icon)) tmp.Icon = string.Format(", icon:\"{0}\"", tmp.Icon.Split(',')[0]);
                        }
                        else
                        {
                            tmp.Icon = string.Format(", icon:\"{0}\"", _iconUrl);
                        }
                    }

                    if (hasTitle) tmp.Title = (string)row[_titleColumnName];
                    if (hasDescription) tmp.Description = row[_descrColumnName].ToString();

                    if (tmp.Latitude != 0.0 && tmp.Longitude != 0.0) tmps.Add(tmp);

                }
                if (tmps.Count > 0) _markers.AddRange(tmps);
            }

        }

        /// <summary>
        ///   Fill marker's coordinates from source data row
        /// </summary>
        private void FillCoordinates(DataRow row, Marker marker)
        {
                double dPos;
                bool lngFound = false;
                string strPos = row[_latitudeColumnName].ToString();
                bool parseOk = double.TryParse(strPos, NumberStyles.Number, CultureInfo.CurrentCulture, out dPos);
                if (!parseOk) parseOk = double.TryParse(strPos, NumberStyles.Number, CultureInfo.InvariantCulture, out dPos);
                if (!parseOk)
                {
                    var r =
                        new Regex(
                            @"(?<first>(((?<degrees>\d+)\xb0(?<minutes>\d+)[′'](?<seconds>\d+)[″\x22])(?<quadrant>[NSEW]))|((?<degrees>\d+\x2E\d+)(?<quadrant>[NSEW]))|((?<sign>[-]){0,1}(?<degrees>\d+\x2E\d+))|((?<sign>[-]){0,1}(?<degrees>\d+)\xb0\s(?<minutes>\d+\x2E\d+))|(((?<degrees>\d+)d\s(?<minutes>\d+)[′']\s(?<seconds>\d+)[″\x22]\s)(?<quadrant>[NSEW])))((,{0,1}\s)(?<lng>(((?<degrees>\d+)\xb0(?<minutes>\d+)[′'](?<seconds>\d+)[″\x22])(?<quadrant>[NSEW]))|((?<degrees>\d+\x2E\d+)(?<quadrant>[NSEW]))|((?<sign>[-]){0,1}(?<degrees>\d+\x2E\d+))|((?<sign>[-]){0,1}(?<degrees>\d+)\xb0\s(?<minutes>\d+\x2E\d+))|(((?<degrees>\d+)d\s(?<minutes>\d+)[′']\s(?<seconds>\d+)[″\x22]\s)(?<quadrant>[NSEW]))){0,1}){0,1}",
                            RegexOptions.Singleline | RegexOptions.ExplicitCapture);
                    Match match = r.Match(strPos);
                    if (match.Success)
                    {
                        string sign = match.Groups["sign"].Success ? match.Groups["sign"].Captures[0].Value : string.Empty;
                        string quadrant = match.Groups["quadrant"].Success
                                              ? match.Groups["quadrant"].Captures[0].Value
                                              : string.Empty;
                        string degrees = match.Groups["degrees"].Captures[0].Value;
                        string minutes = match.Groups["minutes"].Success
                                             ? match.Groups["minutes"].Captures[0].Value
                                             : "0";
                        string seconds = match.Groups["seconds"].Success
                                             ? match.Groups["seconds"].Captures[0].Value
                                             : "0";
                        marker.Latitude = ConstructCoordinate(sign, quadrant, degrees, minutes, seconds);
                        if (match.Groups["lng"].Success)
                        {
                            lngFound = true;
                            sign = match.Groups["sign"].Success ? match.Groups["sign"].Captures[1].Value : string.Empty;
                            quadrant = match.Groups["quadrant"].Success ? match.Groups["quadrant"].Captures[1].Value : string.Empty;
                            degrees = match.Groups["degrees"].Captures[1].Value;
                            minutes = match.Groups["minutes"].Success ? match.Groups["minutes"].Captures[1].Value : "0";
                            seconds = match.Groups["seconds"].Success ? match.Groups["seconds"].Captures[1].Value : "0";
                            marker.Longitude = ConstructCoordinate(sign, quadrant, degrees, minutes, seconds);
                        }
                    }
                }
                else marker.Latitude = dPos;
                if (!lngFound)
                {
                    strPos = row[_longitudeColumnName].ToString();
                    parseOk = double.TryParse(strPos, NumberStyles.Number, CultureInfo.CurrentCulture, out dPos);
                    if (!parseOk) parseOk = double.TryParse(strPos, NumberStyles.Number, CultureInfo.InvariantCulture, out dPos);
                    if (!parseOk)
                    {
                        var r =
                            new Regex(
                                @"(?<first>(((?<degrees>\d+)\xb0(?<minutes>\d+)[′'](?<seconds>\d+)[″\x22])(?<quadrant>[NSEW]))|((?<degrees>\d+\x2E\d+)(?<quadrant>[NSEW]))|((?<sign>[-]){0,1}(?<degrees>\d+\x2E\d+))|((?<sign>[-]){0,1}(?<degrees>\d+)\xb0\s(?<minutes>\d+\x2E\d+))|(((?<degrees>\d+)d\s(?<minutes>\d+)[′']\s(?<seconds>\d+)[″\x22]\s)(?<quadrant>[NSEW])))((,{0,1}\s)(?<lng>(((?<degrees>\d+)\xb0(?<minutes>\d+)[′'](?<seconds>\d+)[″\x22])(?<quadrant>[NSEW]))|((?<degrees>\d+\x2E\d+)(?<quadrant>[NSEW]))|((?<sign>[-]){0,1}(?<degrees>\d+\x2E\d+))|((?<sign>[-]){0,1}(?<degrees>\d+)\xb0\s(?<minutes>\d+\x2E\d+))|(((?<degrees>\d+)d\s(?<minutes>\d+)[′']\s(?<seconds>\d+)[″\x22]\s)(?<quadrant>[NSEW]))){0,1}){0,1}",
                                RegexOptions.Singleline | RegexOptions.ExplicitCapture);
                        Match match = r.Match(strPos);
                        if (match.Success)
                        {
                            string sign = match.Groups["sign"].Success ? match.Groups["sign"].Captures[0].Value : string.Empty;
                            string quadrant = match.Groups["quadrant"].Success
                                                  ? match.Groups["quadrant"].Captures[0].Value
                                                  : string.Empty;
                            string degrees = match.Groups["degrees"].Captures[0].Value;
                            string minutes = match.Groups["minutes"].Success
                                                 ? match.Groups["minutes"].Captures[0].Value
                                                 : "0";
                            string seconds = match.Groups["seconds"].Success
                                                 ? match.Groups["seconds"].Captures[0].Value
                                                 : "0";
                            marker.Longitude = ConstructCoordinate(sign, quadrant, degrees, minutes, seconds);
                        }
                    }
                    else marker.Longitude = dPos;
                }
        }

        /// <summary>
        ///   Construct decimal representation used by Google from structured form of coordinate
        /// </summary>
        private static double ConstructCoordinate(string sign, string quadrant, string degrees, string minutes, string seconds)
        {
            double d = double.Parse(degrees);
            double m = double.Parse(minutes);
            double s = double.Parse(seconds);
            double retValue = d + m / 60 + s / 3600;
            if (sign == "-" || quadrant == "S" || quadrant == "W") retValue *= -1;
            return retValue;
        }

        public override EditorPartCollection CreateEditorParts()
        {
            var editorParts = new ArrayList();
            var editor = new MapEditorPart { ID = ID + "_editorPart" };
            editorParts.Add(editor);
            return new EditorPartCollection(editorParts);
        }

        protected override void CreateChildControls()
        {
            try
            {
                if (_clusterer)
                {
                    Controls.Add(new LiteralControl(
                        string.Format(CultureInfo.InvariantCulture, "<script type=\"text/javascript\" src=\"http{0}://google-maps-utility-library-v3.googlecode.com/svn/trunk/markerclusterer/src/markerclusterer_compiled.js\"></script>", Https ? "s" : string.Empty)));
                }

                GetData();

                if (_centreLongitude == 0 && _centreLatitude == 0) CalculateCentre(out _centreLatitude, out _centreLongitude);

                var canvas =
                    new LiteralControl(
                        string.Format("<meta name=\"viewport\" content=\"initial-scale=1.0, user-scalable=no\" /><script type=\"text/javascript\" src=\"{2}/maps/api/js{3}\"></script><div id=\"map_canvas\" style=\"{0}height:{1}px;\"></div>",
                            _width == 0 ? String.Empty : "width:" + _width + "px;", _height, ApiUrl, string.IsNullOrEmpty(_apiKey) ? string.Empty : "?key=" + _apiKey));
                var scripts =
                    new LiteralControl(
                        string.Format(CultureInfo.InvariantCulture, "<script type=\"text/javascript\">var map;\nfunction ginitialize() {{ var myLatlng = new google.maps.LatLng({0}, {1});var myOptions = {{mapTypeControlOptions: {{style: google.maps.MapTypeControlStyle.DROPDOWN_MENU}},zoom: {2}, center: myLatlng,mapTypeId: google.maps.MapTypeId.{3}, panControl: {4}, zoomControl: {5}, mapTypeControl: {6}, streetViewControl :{7} }};map = new google.maps.Map(document.getElementById(\"map_canvas\"), myOptions);}}\n ginitialize();</script>",
                            _centreLatitude, _centreLongitude, _zoom, _mapType, _panControl ? "true" : "false", _zoomControl ? "true" : "false", _maptypeControl ? "true" : "false", _streetviewControl ? "true" : "false"));

                Controls.Add(canvas);
                Controls.Add(scripts);

                GenerateMarkersScript(_markers.ToArray());
            }
            catch (Exception ex)
            {
                if (!DesignMode) this.Page.ClientScript.RegisterStartupScript(this.GetType(), "error", string.Format("<script>window.alert(\"{0}\")</script>", ex.Message.Replace("\"", "\\\"")));
            }
        }

        private void GenerateMarkersScript(Marker[] markers)
        {
            if (markers == null || markers.Length == 0) return;

            var itemsSetupScript = new StringBuilder("<script type=\"text/javascript\">var infoW = null;var markers = [];");

            for (int i = 0; i < markers.Length; i++)
            {
                Marker marker = markers[i];
                itemsSetupScript.AppendFormat(CultureInfo.InvariantCulture,
                    "var mr{3} = new google.maps.Marker({{position: new google.maps.LatLng({1}, {2}),{5} title: \"{0}\" {4}}});",
                    string.IsNullOrEmpty(marker.Title) ? string.Empty : marker.Title.Replace('\n', ' ').Replace("\"", "\\\""),
                    marker.Latitude,
                    marker.Longitude,
                    i,
                    string.IsNullOrEmpty(marker.Icon) ? string.Empty : marker.Icon,
                    _clusterer ? string.Empty : "map : map,");

                if (!string.IsNullOrEmpty(marker.Description)) itemsSetupScript.AppendFormat(
                    CultureInfo.InvariantCulture,
                    "google.maps.event.addListener(mr{1}, 'click', function () {{if (infoW!=null) infoW.close(); infoW = new google.maps.InfoWindow({{content: \"{0}\"}});infoW.open(map, mr{1});}});",
                    marker.Description.Replace('\n', ' ').Replace('\r', ' ').Replace("\"", "\\\""),
                    i);
                if (_clusterer)
                {
                    itemsSetupScript.AppendFormat("markers.push(mr{0});",i);
                }

            }
            if (_clusterer)
            {
                itemsSetupScript.Append("var markerCluster = new MarkerClusterer(map, markers);");
            }
            itemsSetupScript.Append("</script>");
            
            Controls.Add(new LiteralControl(itemsSetupScript.ToString()));
        }

        /// <summary>
        ///   When center point is not configured, this method is used to calculate it using found markers
        /// </summary>
        private void CalculateCentre(out double cLat, out double cLng)
        {
            double maxLat = -200, minLat = 200;
            double maxLng = -200, minLng = 200;

            foreach (Marker marker in _markers)
            {
                if (maxLat < marker.Latitude) maxLat = marker.Latitude;
                if (minLat > marker.Latitude) minLat = marker.Latitude;
                if (maxLng < marker.Longitude) maxLng = marker.Longitude;
                if (minLng > marker.Longitude) minLng = marker.Longitude;
            }

            cLat = (maxLat + minLat) / 2;
            cLng = (maxLng + minLng) / 2;
        }

    }
}
